package hardwareToSQL;


import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

import java.io.BufferedReader;
import java.io.InputStreamReader;

import java.net.ServerSocket;
import java.net.Socket;

import java.util.LinkedList;
import java.util.NoSuchElementException;
import java.util.Stack;
import java.util.Properties;
import java.util.Hashtable;


public class DataCollector extends Thread{

	/**Buffer containing the retrieved strings ready to be sent out */
	private static Stack<SensorData> dataBuffer;
	/**Variable storing the old data if no new data can be given */
	private SensorData prevSensorData;
	/**Buffer for storing the name and values of the strings of data */
	private char[] d_id, d_value;

	/**Properties defining the string format */
	private Properties stringFormat;
	/**Properties defining the names corresponding to the IDs of given data */
	private Properties dataTypeNames;
	/**Properties defining the connection for server and clients */
	private Properties connectionConfig;
	/**Hash-table storing the ID values corresponding to the names */
	private Hashtable<String, String> idNames;

	/**The parameters defining the form of the input strings */
	private int xLength; 
	private int yLength;
	private int zLength;
	private String symbol;
	/**Object representing the separator */
	private Separator separator;

	/**Server socket */
	private ServerSocket serverSocket;
	/**Client socket */
	private Socket clientSocket;
	/**Input reader for the socket */
	private BufferedReader input;

	/**The size of the test buffer required before data can start being
	 * read from it. This is because due to the linked list used, the
	 * size of the buffer might increase to a certain size first before
	 * actually holding in any elements, hence resulting in an error
	 * when checking for a size of zero, say for example. */
	private final int safeBufferSize = 200;
	
	/**Variable indicating that test data is being read directly from a buffer */
	private boolean testDataDirectMode = false;
	/**Variable indicating that test data is being read from a modem */
	@SuppressWarnings("unused")
	private boolean testDataModemMode = false;
	/**Variable indicating that data is being read from a simulator */
	@SuppressWarnings("unused")
	private boolean simulationMode = false;
	/**Variable indicating that normal data is being read from a hardware */
	@SuppressWarnings("unused")
	private boolean normalMode = false;

	/**The buffer the data collector will read from if expecting direct test
	 * data, but it not coming through the modem */
	private LinkedList<Character> testBuffer;

	public int bytesReceived=0;
	public int dataReceived=0;

	/**Class contructor for normal hardware input*/
	public DataCollector() {
		normalMode = true;

		/**Initialize the variables for data collection */
		init();
	}

	/**Class constructor for testing with input coming in directly */
	public DataCollector(LinkedList <Character> testBuffer){
		testDataDirectMode = true;
		this.testBuffer = testBuffer;
		/**Initialize the variables for data collection */
		init();
	}

	/**Initialize the class object */
	private void init() {

		/**Load the properties */
		try {
			stringFormat = new Properties();
			dataTypeNames = new Properties();
			connectionConfig = new Properties();
			stringFormat.load(new FileInputStream
					("properties/stringFormat.properties"));
			dataTypeNames.load(new FileInputStream
					("properties/dataTypeNames.properties"));
			connectionConfig.load(new FileInputStream
					("properties/connection_config.properties"));
		} catch (FileNotFoundException e) {
			System.err.print("\nFailed to load property file\n");
			System.exit(1);
		} catch (IOException e) {
			System.err.print("\nFailed to load property file\n");
			System.exit(1);
		} catch (NullPointerException e) {
			System.err.print("\nFailed to load property file\n");
			System.exit(1);
		}

		/**Set default value to sensor data object so that it can
		 * be given when no data is yet available */
		prevSensorData = new SensorData(SensorData.DEFAULT, 0, "");

		/**Set the properties that define the input data */
		symbol  = stringFormat.getProperty("Symbol");
		xLength  = Integer.parseInt(stringFormat.getProperty("Separator"));
		yLength  = Integer.parseInt(stringFormat.getProperty("ID"));
		zLength  = Integer.parseInt(stringFormat.getProperty("Value"));

		/**Define the separator */
		separator = new Separator(xLength, symbol);

		/**Initialize the hash-table */
		idNames = new Hashtable<String, String>();
		try {
			for(int i = 0; i < Integer.parseInt(dataTypeNames.
					getProperty("numOfTypes")); i++){
				idNames.put(dataTypeNames.getProperty("ID" + (i+1)), 
						dataTypeNames.getProperty("NAME" + (i+1)));
			}
		} catch (NullPointerException e) {
			System.err.print("\nFailed to initialize correspondence between" + 
			"string IDs and the corresponding names\n");
			System.exit(1);
		}

		/**Initialize the buffers for storing the names and values of input */
		dataBuffer = new Stack<SensorData>();
		d_id = new char[yLength];
		d_value = new char[zLength];
	}

	/**Establish connection with the hardware */
	private void makeConnection(boolean isServerMode) throws IOException {
		String address = connectionConfig.getProperty("Server_Address");
		int port = Integer.parseInt(connectionConfig.getProperty("Server_port"));
		if (isServerMode) {
			/**Connect server to the port if in server mode */
			try {
				System.out.print("\nWaiting to establish server\n");

				serverSocket = new ServerSocket(port);

				System.out.print("\nEstablished server socket connction to port\n");
			} catch (IOException e) {
				System.err.print("\nFailed to establish server socket\n");
				System.exit(1);
			}
			/**Connect to the client */
			try {
				System.out.print("\nWaiting for client connection\n");

				clientSocket = serverSocket.accept();

				System.out.print("\nEstablished sconnction with client\n");
			} catch (IOException e) {
				System.err.print("\nFailed to initialize connection for the "
						+ "client\n");
				System.exit(1);
			} catch (NullPointerException e) {
				System.err.print("\nFailed to initialize connection for the "
						+ "client\n");
				System.exit(1);
			}
		}else{
			try {
				System.out.printf("\nConnecting to Server %s:%d\n",address,port);

				clientSocket = new Socket(address,port);

				System.out.print("\nEstablished sconnction with server\n");
			} catch (IOException e) {
				System.err.print("\nFailed to initialize connection for the "
						+ "server\n");
				System.exit(1);
			} catch (NullPointerException e) {
				System.err.print("\nFailed to initialize connection for the "
						+ "server\n");
				System.exit(1);
			}
		}
		/**Establish the input from the client/server */
		try {
			input = new BufferedReader( new InputStreamReader(
					clientSocket.getInputStream()));
		} catch (NullPointerException e) {
			System.err.print("\nFailed to create input reader for client\n");
			System.exit(1);
		} catch (IOException e) {
			System.err.print("\nFailed to create input reader for client\n");
			System.exit(1);
		} 
	}


	/**Start a new thread to deal with the process of reading data */
	public void run() {

		/**Initialize the connection to the hardware or any other sourece
		 * where the information will be read from */
		if(testDataDirectMode){

		} else {
			try {
				makeConnection(false);
			} catch (IOException e) {
				System.err.print("\nFailed to set up server network\n");
				System.exit(1);
			}
		}
		/**Continue to process new data input */
		while(true){
			if(testDataDirectMode){
				if(testBuffer.size() > safeBufferSize){
					/**Check for separator to see whether or not to read a new string */
					try {
						bytesReceived += 1;/**For the read in the if statement */
						if(separator.update((char)testBuffer.pop()) ) {

							/**Once the separator is found, read a string of bytes 
							 * containing the string name and the value it holds */
							for(int i=0; i< yLength; i++){
								d_id[i] = testBuffer.pop();
							}
							for(int i=0; i< zLength; i++){
								d_value[i] = testBuffer.pop();
							}
						
							bytesReceived += yLength+zLength;/**Keep track of the bytes read */

							/**Store the data as a copy so that if the buffer becomes
							 * empty, this value will always be returned */
							prevSensorData.update(idNames.get(new String(d_id)), 
									Integer.parseInt(new String(d_value)), "");

							/**Store the data into the buffer as an object */
							dataBuffer.push
							(new SensorData(idNames.get(new String(d_id)), 
									Integer.parseInt(new String(d_value)), ""));

							dataReceived += 1;/**Keep track of the data pieces received */
						}
					} 
					catch (NoSuchElementException e) {
						System.err.print("\nFailed to read input from data generator\n");
					} catch (NullPointerException e) {
						System.err.print("\nFailed to read input from data generator\n");
					}
				}
				
			} else {

				/**Check for separator to see whether or not to read a new string */
				try {
					bytesReceived += 1;/**For the read in the if statement */
					if(separator.update((char)input.read()) ) {

						/**Once the separator is found, read a string of bytes 
						 * containing the string name and the value it holds */
						input.read(d_id, 0, yLength);
						input.read(d_value, 0, zLength);

						bytesReceived += yLength+zLength;/**Keep track of the bytes read */

						/**Store the data as a copy so that if the buffer becomes
						 * empty, this value will always be returned */
						prevSensorData.update(idNames.get(new String(d_id)), 
								Integer.parseInt(new String(d_value)), "");

						/**Store the data into the buffer as an object */
						dataBuffer.push
						(new SensorData(idNames.get(new String(d_id)), 
								Integer.parseInt(new String(d_value)), ""));

						dataReceived += 1;/**Keep track of the data pieces received */
					}
				} 
				catch (NumberFormatException e) {
					System.err.print("\nFailed to read input from hardware\n");
				} catch (IOException e) {
					System.err.print("\nFailed to read input from hardware\n");
				} catch (NullPointerException e) {
					System.err.print("\nFailed to read input from hardware\n");
				}
			}
		}
	}

	/**Return the object containing the data that has been received */
	public SensorData getNextData() {
	
		//Return the old object if no new data is yet available
		if(dataBuffer.size() > safeBufferSize) {
			return dataBuffer.pop();
		}

		//Otherwise return the new data
		else {
			return prevSensorData;
		}
	}

}